#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include "common.h"
#include "logger.h"
#include "ctrl_channel.h"
#include "data_channel.h"
#include "file.h"

#ifdef _WIN32
#include <crtdbg.h>
#endif

#define MAX_CONNECTIONS 100

enum {
	SERVER_ERROR = -1,
	SERVER_SUCCESS = 0
};

typedef struct command_t {
	int mode;
	int command;
	FILE* local_file;
} command_t;

logger_t log;

static int listen_on_port(int port);
static void accept_connection(link_t* remote, void* arg, int* stop_listen);
static void* serve_client(void* arg);
static void read_command(link_t* ctrl, int mode, void* arg, int* abort);
static int file_transfer(link_t * ctrl, link_t* data, int cmd, FILE* file);

int main(int argc, char** argv)
{
	if (argc != 3) {
		DPRINT("[s] usage: %s port logfile", argv[0]);
		return -1;
	}

	if (logger_init(&log, argv[2]) != LOGGER_SUCCESS) {
		DPRINT("[s] couldn't open log file: %s", argv[2]);
		goto out;
	}

	DPRINT("[s] starting... (ctrl+c to exit)");

	if (LINK_SUCCESS != link_init()) {
		goto clean;
	}

	if (SERVER_SUCCESS != listen_on_port(atoi(argv[1]))) {
		goto clean;
	}

clean:
	logger_destroy(&log);

out:

	check_leaks();

	return 0;
}

static int listen_on_port(int port)
{
	link_t local;

	local.port = port;
	if (link_listen(&local, MAX_CONNECTIONS) != LINK_SUCCESS) {
		DPRINT("[s] failed to listen on port");
		return SERVER_ERROR;
	}

	if (link_accept(&local, accept_connection, NULL) != LINK_SUCCESS) {
		DPRINT("[s] failed to accept connections");
		return SERVER_ERROR;
	}

	return SERVER_SUCCESS;
}

static void accept_connection(link_t* remote, void* arg, int* stop_listen)
{
	thread_t thread;
	link_t* remote_copy;
	pthread_attr_t attr;

	remote_copy = (link_t*) malloc(sizeof(link_t));
	if (remote_copy == NULL) {
		DPRINT("[s] failed to copy client data");
		return;
	}

	remote_copy->sockfd = remote->sockfd;
	remote_copy->addr = remote->addr;
	remote_copy->port = remote->port;

	if (pthread_attr_init(&attr) != 0) {
		DPRINT("[s] failed to init pthread attr");
		free(remote_copy);
		return;
	}

	if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) {
		DPRINT("[s] failed to set pthread attr");
		pthread_attr_destroy(&attr);
		free(remote_copy);
		return;
	}

	if (pthread_create(&thread, &attr, serve_client, remote_copy) != 0) {
		DPRINT("[s] failed to create client thread");
		pthread_attr_destroy(&attr);
		free(remote_copy);
	}

	if (pthread_attr_destroy(&attr) != 0) {
		DPRINT("[s] failed to destroy pthread attr");
	}

	/* remote_copy will be freed by the serve_client thread */
}

static void* serve_client(void* arg)
{
	link_t* ctrl = (link_t *) arg, data;
	command_t cmd;
	int drc;

	assert(ctrl != NULL);

	cmd.local_file = NULL;

	drc = data_setup_slave(ctrl, &data, read_command, &cmd);

	if (drc != DATA_SUCCESS) {
		if (drc == DATA_ABORTED) {
			DPRINT("[s] data link aborted");
		}
		else {
			ctrl_message msg;

			msg.ctrl = ERR_OPEN_DATA;
			DPRINT("[s] failed to establish data link, sending ERR_OPEN_DATA");
			if (ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS) {
				DPRINT("[s] failed to send ERR_OPEN_DATA");
			}
		}

		goto clean;
	}

	DPRINT("[s] data link established");

	if (file_transfer(ctrl, &data, cmd.command, cmd.local_file) == SERVER_ERROR) {
		goto clean;
	}

	DPRINT("[s] done executing clients command");

clean:
	if (cmd.local_file != NULL) {
		fclose(cmd.local_file);
	}

	link_close(ctrl); /* data is closed by file_transfer */
	free(ctrl);

	return NULL;
}

static void read_command(link_t* ctrl, int mode, void* arg, int* abort)
{
	command_t* cmd = (command_t *) arg;
	ctrl_message msg, reply;
	int ret = CTRL_ERROR;

	/* recv cmd with filename */
	DPRINT("[s] waiting for command ...");

	ret = ctrl_send_receive(ctrl, NULL, &reply);
	if (ret == CTRL_ERROR) {
		DPRINT("[s] FAILED (couldn't receive command)");
		goto err;
	}
	else if ((ret == CTRL_PARSE_ERROR) || ((reply.ctrl != RETR) && (reply.ctrl != STOR))) {
		DPRINT("[s] FAILED (did not receive a valid command");

		msg.ctrl = SYNTAX_ERROR;
		DPRINT("[s] sending systax_error");
		if (ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS) {
			DPRINT("[s] failed to send SYNTAX_ERROR");
		}

		goto err;
	}

	DPRINT("[s] opening file '%s' ...", reply.filename);

	cmd->command = reply.ctrl;
	cmd->local_file = fopen(reply.filename, cmd->command == STOR ? "wb" : "rb");

	if (cmd->local_file == NULL) { /* file does not exist */
		DPRINT("[s] FAILED (couldn't open local file)");

		msg.ctrl = ERR_NO_ACTION;
		DPRINT("[s] sending ERR_NO_ACTION");
		if (ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS) {
			DPRINT("[s] failed to send ERR_NO_ACTION");
		}

		goto err;
	}

	DPRINT("[s] OK");

	if (mode == ACTIVE) {
		msg.ctrl = OPEN_DATA;
		DPRINT("[s] sending OPEN_DATA");
		if (ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS) {
			DPRINT("[s] failed to send OPEN_DATA");
			goto err;
		}
	}

	return;

err:
	*abort = 1;
}

static int file_transfer(link_t * ctrl, link_t* data, int cmd, FILE* file)
{
	ctrl_message msg;

	switch (cmd) {
	case RETR:
		DPRINT("[s] sending file...");

		if (FILE_SUCCESS != file_send(data, file)) {
			DPRINT("[s] FAILED (file_send failed)");
			goto cmd_err;
		}

		break;

	case STOR:
		DPRINT("[s] receiving file...");

		if (FILE_SUCCESS != file_recv(data, file)) {
			DPRINT("[s] FAILED (file_recv failed)");
			goto cmd_err;
		}

		break;

	default:
		assert(0);
		return SERVER_ERROR;
	}

	DPRINT("[s] OK. sending DATA_FIN");

	msg.ctrl = DATA_FIN;
	if (ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS) {
		DPRINT("[s] FAILED (couldn't send DATA_FIN )");
		return DATA_ERROR;
	}

	DPRINT("[s] sent DATA_FIN");

	return SERVER_SUCCESS;

cmd_err:
	DPRINT("[c] closing data connecting");
	link_close(data);

	/* send an error */
	DPRINT("[s] sending ERR_DATA_CLOSED");

	msg.ctrl = ERR_DATA_CLOSED;
	if ((ctrl_send_receive(ctrl, &msg, NULL) != CTRL_SUCCESS)) {
		DPRINT("[s] couldn't send ERR_DATA_CLOSED to server");
	}

	return SERVER_ERROR;
}
